﻿// <copyright file="EntityFrameworkGenerator.cs" company="T4 Toolbox Team">
//  Copyright © T4 Toolbox Team. All Rights Reserved.
// </copyright>

namespace T4Toolbox.EntityFramework
{
    using System;
    using System.CodeDom.Compiler;
    using System.Collections.Generic;
    using System.Data.Entity.Design;
#if DEV10
    using System.Data.Entity.Design.PluralizationServices;
#endif 
    using System.Data.Metadata.Edm;
    using System.Data.SqlClient;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Xml;

    /// <summary>
    /// Generates Entity Framework DAL from a database.
    /// </summary>
    public class EntityFrameworkGenerator : Generator
    {
        /// <summary>
        /// Stores filter entries.
        /// </summary>
        private List<EntityStoreSchemaFilterEntry> filters;

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityFrameworkGenerator"/> class.
        /// </summary>
        public EntityFrameworkGenerator()
        {
            this.Provider = "System.Data.SqlClient";
            this.Namespace = TransformationContext.DefaultNamespace;
            this.filters = new List<EntityStoreSchemaFilterEntry>();
#if DEV10
            this.Pluralize = true;
#endif

            this.ClassesTemplate = new EntityFrameworkClassesTemplate();
            this.ConceptualModelTemplate = new EntityFrameworkConceptualModelTemplate();
            this.MappingModelTemplate = new EntityFrameworkMappingModelTemplate();
            this.StorageModelTemplate = new EntityFrameworkStorageModelTemplate();
            this.ViewsTemplate = new EntityFrameworkViewsTemplate();
        }

        /// <summary>
        /// Gets or sets the name of ADO.NET data provider that will be used to connect 
        /// to the database.
        /// </summary>
        /// <value>
        /// A <see cref="String"/> that contains a valid ADO.NET data provider name,
        /// "System.Data.SqlClient" by default.
        /// </value>
        public string Provider { get; set; }

        /// <summary>
        /// Gets or sets the ADO.NET connection string that will be used to connect to 
        /// the database.
        /// </summary>
        /// <value>
        /// A <see cref="String"/> that contains a valid ADO.NET connection string. 
        /// </value>
        /// <remarks>
        /// Connection string will be built automatically if you specify <see cref="DatabaseFile"/>.
        /// </remarks>
        public string ConnectionString { get; set; }

        /// <summary>
        /// Gets or sets database file name.
        /// </summary>
        /// <value>
        /// A <see cref="String"/> that contains a valid path to a SQL Server data (.mdf) file.
        /// </value>
        /// <remarks>
        /// When a database file is specified, the <see cref="ConnectionString"/> will be built 
        /// automatically and will use SQL Server Express instance ".\SQLEXPRESS" to attach the 
        /// specified database in user mode and connect to it using integrated security.
        /// </remarks>
        public string DatabaseFile { get; set; }

        /// <summary>
        /// Gets or sets name of the generated object context class.
        /// </summary>
        /// <value>
        /// A <see cref="String"/> that contains a valid class name. By default, entity container 
        /// name is determined based on the name of the database plus the word "Entities", i.e. 
        /// "NorthwindEntities".
        /// </value>
        public string EntityContainerName { get; set; }

        /// <summary>
        /// Gets a collection of filter entries that determine tables, views and stored procedures that
        /// will be imported from the database.
        /// </summary>
        /// <value>
        /// A collection of <see cref="EntityStoreSchemaFilterEntry"/> objects.
        /// </value>
        public ICollection<EntityStoreSchemaFilterEntry> Filters
        {
            get { return this.filters; }
        }

#if DEV10
        /// <summary>
        /// Gets or sets a value indicating whether foreign key properties will be generated
        /// in entity classes.
        /// </summary>
        /// <value>
        /// True, which indicates that type foreign key properties should be generated, or false.
        /// </value>
        public bool GenerateForeignKeyProperties { get; set; }
#endif

        /// <summary>
        /// Gets or sets the programming language for generating entity classes and views.
        /// </summary>
        /// <value>
        /// A <see cref="LanguageOption"/> value, 
        /// <see cref="System.Data.Entity.Design.LanguageOption.GenerateCSharpCode"/> or
        /// <see cref="System.Data.Entity.Design.LanguageOption.GenerateVBCode"/>.
        /// </value>
        public LanguageOption LanguageOption { get; set; }

        /// <summary>
        /// Gets or sets namespace where the classes will be generated.
        /// </summary>
        /// <value>
        /// A <see cref="String"/> that contains a valid C# or Visual Basic namespace name.
        /// </value>
        public string Namespace { get; set; }

#if DEV10
        /// <summary>
        /// Gets or sets a value indicating whether plurlization rules will be applied to 
        /// type and property names.
        /// </summary>
        /// <value>
        /// True, which indicates that type names should be converted to singular form and 
        /// collection property names should be converted to plural form, or false, which 
        /// indicates that type names and collection property names should not be changed.
        /// </value>
        public bool Pluralize { get; set; }
#endif

        /// <summary>
        /// Gets or sets the template that will be used to generate Entity Framework classes.
        /// </summary>
        /// <value>
        /// An <see cref="EntityFrameworkClassesTemplate"/> object.
        /// </value>
        public EntityFrameworkClassesTemplate ClassesTemplate { get; set; }

        /// <summary>
        /// Gets or sets the template that will be used to generate Entity Framework conceptual model.
        /// </summary>
        /// <value>
        /// An <see cref="EntityFrameworkConceptualModelTemplate"/> object.
        /// </value>
        public EntityFrameworkConceptualModelTemplate ConceptualModelTemplate { get; set; }

        /// <summary>
        /// Gets or sets the template that will be used to generate Entity Framework mapping model.
        /// </summary>
        /// <value>
        /// An <see cref="EntityFrameworkMappingModelTemplate"/> object.
        /// </value>
        public EntityFrameworkMappingModelTemplate MappingModelTemplate { get; set; }

        /// <summary>
        /// Gets or sets the template that will be used to generate Entity Framework storage model.
        /// </summary>
        /// <value>
        /// An <see cref="EntityFrameworkStorageModelTemplate"/> object.
        /// </value>
        public EntityFrameworkStorageModelTemplate StorageModelTemplate { get; set; }

        /// <summary>
        /// Gets or sets the template that will be used to generate Entity Framework views.
        /// </summary>
        /// <value>
        /// An <see cref="EntityFrameworkViewsTemplate"/> object.
        /// </value>
        public EntityFrameworkViewsTemplate ViewsTemplate { get; set; }

        /// <summary>
        /// Reports Entity Framework errors.
        /// </summary>
        /// <param name="reportedErrors">Errors reported by Entity Framework.</param>
        /// <param name="displayErrors">Errors that will be displayed by Visual Studio.</param>
        internal static void HandleErrors(IEnumerable<EdmSchemaError> reportedErrors, CompilerErrorCollection displayErrors)
        {
            foreach (EdmSchemaError reportedError in reportedErrors)
            {
                CompilerError displayError = new CompilerError();
                displayError.ErrorText = reportedError.Message;
                displayError.IsWarning = reportedError.Severity == EdmSchemaErrorSeverity.Warning;
                displayErrors.Add(displayError);
            }
        }

        /// <summary>
        /// Generates code.
        /// </summary>
        protected override void RunCore()
        {
            string baseFileName = Path.GetFileNameWithoutExtension(TransformationContext.Host.TemplateFile);

            // Generate storage model from the database
            this.StorageModelTemplate.Provider = this.Provider;
            this.StorageModelTemplate.ConnectionString = this.ConnectionString;
            this.StorageModelTemplate.Namespace = this.Namespace;
            this.StorageModelTemplate.Filters.Clear();
            foreach (EntityStoreSchemaFilterEntry filter in this.Filters)
            {
                this.StorageModelTemplate.Filters.Add(filter);
            }

            this.StorageModelTemplate.RenderToFile(baseFileName + ".ssdl");
            if (this.StorageModelTemplate.Errors.HasErrors)
            {
                return;
            }

            // Load storage model from the generated XML
            EntityModelSchemaGenerator generator = this.CreateModelSchemaGenerator();
            if (this.Errors.HasErrors)
            {
                return;
            }

            // Generate conceptual model from storage model
            this.ConceptualModelTemplate.EntityModelSchemaGenerator = generator;
            this.ConceptualModelTemplate.RenderToFile(baseFileName + ".csdl");

            // Generate mapping model from storage model
            this.MappingModelTemplate.EntityModelSchemaGenerator = generator;
            this.MappingModelTemplate.RenderToFile(baseFileName + ".msl");

            // Generate classes from conceptual model
            this.ClassesTemplate.LanguageOption = this.LanguageOption;
            this.ClassesTemplate.ConceptualModel = this.ConceptualModelTemplate.ConceptualModel;
            this.ClassesTemplate.Render();

            // Generate views
            this.ViewsTemplate.LanguageOption = this.LanguageOption;
            this.ViewsTemplate.ConceptualModel = this.ConceptualModelTemplate.ConceptualModel;
            this.ViewsTemplate.MappingModel = this.MappingModelTemplate.MappingModel;
            this.ViewsTemplate.StorageModel = this.StorageModelTemplate.StorageModel;
            string extension = this.LanguageOption == LanguageOption.GenerateCSharpCode ? ".cs" : ".vb";
            this.ViewsTemplate.RenderToFile(baseFileName + ".Views" + extension);
        }

        /// <summary>
        /// Validates properties before code generation.
        /// </summary>
        protected override void Validate()
        {
            if (string.IsNullOrEmpty(this.Provider))
            {
                throw new TransformationException("Provider property must be assigned");
            }

            if (string.IsNullOrEmpty(this.ConnectionString) && string.IsNullOrEmpty(this.DatabaseFile))
            {
                throw new TransformationException("Either ConnectionString or DatabaseFile property must be assigned");
            }

            if (!string.IsNullOrEmpty(this.DatabaseFile))
            {
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = @".\SQLEXPRESS";
                builder.AttachDBFilename = TransformationContext.Host.ResolvePath(this.DatabaseFile);
                builder.IntegratedSecurity = true;
                builder.UserInstance = true;
                this.ConnectionString = builder.ConnectionString;
            }

            if (string.IsNullOrEmpty(this.EntityContainerName))
            {
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(this.ConnectionString);
                if (string.IsNullOrEmpty(builder.InitialCatalog))
                {
                    this.EntityContainerName = Path.GetFileNameWithoutExtension(builder.AttachDBFilename);
                }
                else
                {
                    this.EntityContainerName = builder.InitialCatalog;
                }

                this.EntityContainerName = CSharpTemplate.PropertyName(this.EntityContainerName + "Entities");
            }

            if (string.IsNullOrEmpty(this.Namespace))
            {
                throw new TransformationException("Namespace property must be assigned");
            }

            if (this.ClassesTemplate == null)
            {
                throw new TransformationException("ClassesTemplate property must be assigned");
            }

            if (this.ConceptualModelTemplate == null)
            {
                throw new TransformationException("ConceptualModelTemplate property must be assigned");
            }

            if (this.MappingModelTemplate == null)
            {
                throw new TransformationException("MappingModelTemplate property must be assigned");
            }

            if (this.StorageModelTemplate == null)
            {
                throw new TransformationException("StorageModelTemplate property must be assigned");
            }

            if (this.ViewsTemplate == null)
            {
                throw new TransformationException("ViewsTemplate property must be assigned");
            }
        }

        /// <summary>
        /// Creates a model schema generator using generated storage model.
        /// </summary>
        /// <returns>
        /// An <see cref="EntityModelSchemaGenerator"/> that can be used for generating CSDL and MSL.
        /// </returns>
        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "stringReader is disposed only once")]
        private EntityModelSchemaGenerator CreateModelSchemaGenerator()
        {
            // Re-hydrate EntityContainer
            EntityContainer entityContainer;
            using (StringReader stringReader = new StringReader(this.StorageModelTemplate.StorageModel))
            using (XmlReader xmlReader = XmlReader.Create(stringReader, new XmlReaderSettings() { CloseInput = false }))
            {
                IList<EdmSchemaError> errors;
                StoreItemCollection stores = MetadataItemCollectionFactory.CreateStoreItemCollection(
                    new XmlReader[] { xmlReader },
                    out errors);
                HandleErrors(errors, this.Errors);
                entityContainer = stores.GetItems<EntityContainer>()[0];
            }

            // Create generator and load metadata
            EntityModelSchemaGenerator generator = new EntityModelSchemaGenerator(
                entityContainer,
                this.Namespace,
                this.EntityContainerName);

#if DEV10
            generator.GenerateForeignKeyProperties = this.GenerateForeignKeyProperties;
            if (this.Pluralize)
            {
                generator.PluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));
            }
#endif

            generator.GenerateMetadata();

            return generator;
        }
    }
}
